import {
  Group,
  PerspectiveCamera,
  Scene,
  Vector3,
  WebGLRenderer,
  MeshBasicMaterial,
  PointsMaterial,
  BufferAttribute,
  Points,
  BufferGeometry,
  Color,
  Math as TMath,
} from "three";
import TWEEN from "@tweenjs/tween.js";
import {
  EffectComposer,
  RenderPass,
  GodRaysPass,
  KernelSize,
} from "postprocessing";
import animates from "./animate";
import Fadeout from "./animate/Fadeout";
import { getSkyBox } from "./texture";
import { createMesh } from "./mesh";
import { sample, random, getTableData } from "./tool";

const TWEEN2 = new TWEEN.Group();
const Cont = 2048; // Number of elements

export default class Sign3DManager {
  constructor(container, config, users) {
    this.animateIndex = 0;
    this.users = users;

    this.container = container;

    this.currentAnimate = null;
    this.renderer = null;
    this.objects = [];

    // 动画
    this.fadeout = null;
    this.animates = [];

    this.config = {
      ...config,
      tableData: getTableData(config.tableData),
    };
    this.GodRaysPass = null;
    this.AnimationFrame = null;

    this.scene = new Scene();
    this.group = new Group();
    this.init();
  }

  async init() {
    await this.setupBackground();
    if (!this.scene) return;

    this.camera = new PerspectiveCamera(
      40,
      window.innerWidth / window.innerHeight,
      20,
      10000
    );
    this.camera.position.z = 3000;

    this.baseRenderer = new WebGLRenderer({ alpha: true });
    this.baseRenderer.setSize(window.innerWidth, window.innerHeight);
    this.baseRenderer.domElement.style.position = "fixed";
    this.baseRenderer.domElement.style.left = 0;
    this.container.appendChild(this.baseRenderer.domElement);

    this.setupPostProcessing();
    this.prepareAnimate();
    await this.createMeshObjects();

    this.loopAnimate();

    window.addEventListener("resize", this.onResize.bind(this));
    this.animate();
  }

  prepareAnimate() {
    let options = {
      Cont: Cont,
      group: this.group,
      camera: this.camera,
      sign3dConfig: this.config,
    };
    this.fadeout = new Fadeout(options);
    this.animates = this.config.animates.map(
      (animate) => new animates[animate.name](options, this.GodRaysPass)
    );
  }

  async setupBackground() {
    const bgImg = this.config.background;
    if (this.config.backgroundType === "3D") {
      const skyBox = await getSkyBox(bgImg);
      if (!this.scene) {
        return;
      }
      this.scene.add(skyBox);
    } else {
      this.container.style.backgroundImage = `url(${bgImg})`;
    }
  }

  setupPostProcessing() {
    let renderer = this.baseRenderer;

    const sunMaterial = new PointsMaterial({
      size: 0,
      sizeAttenuation: true,
      color: 0xffddaa,
      alphaTest: 0,
      transparent: true,
      fog: false,
    });
    const sunGeometry = new BufferGeometry();
    sunGeometry.addAttribute(
      "position",
      new BufferAttribute(new Float32Array(3), 3)
    );
    const sun = new Points(sunGeometry, sunMaterial);
    sun.frustumCulled = true;
    sun.position.set(0, 0, -100);
    this.scene.add(sun);

    const composer = new EffectComposer(renderer);
    const renderPass = new RenderPass(this.scene, this.camera);
    composer.addPass(renderPass);

    const godRaysPass = new GodRaysPass(this.group, this.camera, sun, {
      resolutionScale: 0.8,
      kernelSize: KernelSize.SMALL,
      intensity: 0.4,
      density: 0.86,
      decay: 0.83,
      weight: 0.4,
      exposure: 0.6,
      samples: 60,
      clampMax: 1.0,
    });

    godRaysPass.renderPassMask = new RenderPass(
      godRaysPass.mainScene,
      godRaysPass.mainCamera,
      {
        overrideMaterial: new MeshBasicMaterial({
          color: this.config.shineColor,
        }),
        clearColor: new Color(0x000000),
      }
    );

    composer.addPass(godRaysPass);
    godRaysPass.renderToScreen = true;

    this.GodRaysPass = godRaysPass;
    this.renderer = composer;
  }

  async createMeshObjects() {
    for (let i = 0; i < Cont; i++) {
      const user = i < this.users.length ? this.users[i] : sample(this.users);
      const object = await createMesh(user, this.config.shape);
      this.group.add(object);
      this.objects.push(object);
    }
    this.scene.add(this.group);
  }

  async addUser(user) {
    let camera = this.camera;

    let object = await createMesh(user, this.config.shape);
    this.scene.add(object);

    // 从当前在摄像头中播放的对象中随机取出一个
    let replaceIndex = random(0, this.currentAnimate.objs.length - 1);
    let replaceObj = this.objects[replaceIndex];

    let porperty = {};

    // 间隔200
    let length = 200;
    let randNum = {
      x: TMath.randInt(-50, 50),
      y: TMath.randInt(-30, 30),
      z: TMath.randInt(-50, 50),
    };
    // 计算物体相对于相机的朝向的 相对
    let CaculatePosition = function (camera, length) {
      let NewVector = new Vector3(0, 0, 1);
      NewVector.applyEuler(camera.rotation);
      NewVector.setLength(length);
      NewVector.subVectors(camera.position, NewVector);
      return NewVector;
    };

    /**
     * 让object跟随相机移动
     * */
    function followCamera(data) {
      // 根据相机朝向 算出物品所在的相对位置
      let relative = CaculatePosition(camera, length);
      for (let index in porperty) {
        for (let type in porperty[index]) {
          if (index === "rotation") {
            object[index][type] =
              ((camera[index][type] - porperty[index][type]) * data.value) /
                100 +
              porperty[index][type];
          }
          if (index === "position") {
            object[index][type] =
              ((relative[type] + randNum[type] - porperty[index][type]) *
                data.value) /
                100 +
              porperty[index][type];
          }
        }
      }
    }

    /**
     * 初始化porperty 便于下次动画计算初始值与结束值
     */
    function initPorperty(world = false) {
      let position = world ? object.getWorldPosition() : object.position;
      let rotation = world ? object.getWorldRotation() : object.rotation;
      porperty = {
        position: {
          x: position.x,
          y: position.y,
          z: position.z,
        },
        rotation: {
          x: rotation.x,
          y: rotation.y,
          z: rotation.z,
        },
      };
    }

    initPorperty();

    // 移动到摄像机前 并跟随摄像机
    let moveTo = new TWEEN.Tween({ value: 0 }, TWEEN2)
      .to({ value: 100 }, 1000)
      .onUpdate((data) => {
        followCamera(data);
      })
      .easing(TWEEN.Easing.Exponential.InOut);
    let wait = new TWEEN.Tween({ value: 100 }, TWEEN2)
      .onStart(initPorperty)
      .to({ value: 100 }, 1000)
      .onUpdate((data) => {
        followCamera(data);
      });

    let showing = new TWEEN.Tween({ value: 0 }, TWEEN2)
      .to({ value: 100 }, 3000)
      .onStart(() => {
        let newPosition = this.group.worldToLocal(object.position);
        let cameraPosition = this.group.worldToLocal(
          new Vector3().copy(camera.position)
        );
        this.group.add(object);
        object.position.copy(newPosition);
        object.lookAt(cameraPosition);
        initPorperty();
        //            object.rotation.copy(camera)
        //            let relative = CaculatePosition(camera, length)
        //            object.position.copy(relative)
        //            object.updateMatrixWorld(true)
      })
      .onUpdate((data) => {
        for (let index in porperty) {
          for (let type in porperty[index]) {
            object[index][type] =
              ((replaceObj[index][type] - porperty[index][type]) * data.value) /
                100 +
              porperty[index][type];
          }
        }
      })
      .onComplete(() => {
        this.group.remove(this.objects[replaceIndex]);
        this.scene.remove(object);
        this.objects[replaceIndex] = object;
      })
      .easing(TWEEN.Easing.Exponential.InOut);

    moveTo.chain(wait);
    wait.chain(showing);

    moveTo.start();
  }

  loopAnimate = () => {
    this.currentAnimate = this.animates[this.animateIndex];
    this.animateIndex = (this.animateIndex + 1) % this.animates.length;
    this.animateInit().then(() => {
      this.transform(this.currentAnimate, 3000);
    });

    this.loopAnimateTimer = setTimeout(() => {
      if (this.animates.length === 1) {
        return;
      }
      this.loopAnimate();
    }, (parseInt(this.config.animateSpendTime) + 5) * 1000);
  };

  animateInit() {
    return new Promise((resolve) => {
      const spendTime = 3000;
      this.transform(this.fadeout, spendTime);

      new TWEEN.Tween(this.group.position)
        .to({ x: 0, y: 0, z: 0 }, spendTime)
        .easing(TWEEN.Easing.Exponential.InOut)
        .start();

      new TWEEN.Tween(this.group.rotation)
        .to({ x: 0, y: 0, z: 0 }, spendTime)
        .easing(TWEEN.Easing.Exponential.InOut)
        .start();

      new TWEEN.Tween(this.camera.rotation)
        .to({ x: 0, y: 0, z: 0 }, spendTime)
        .easing(TWEEN.Easing.Exponential.InOut)
        .start();

      new TWEEN.Tween(this.camera.position)
        .to({ x: 0, y: 0, z: 3000 }, spendTime)
        .onComplete(() => {
          this.GodRaysPass.godRaysMaterial.uniforms.density.value = 0.83;
          this.GodRaysPass.intensity = 0.4;
          resolve();
        })
        .easing(TWEEN.Easing.Exponential.InOut)
        .start();
    });
  }

  transform(target, duration) {
    return new Promise((resolve) => {
      const targets = target.objs;
      TWEEN.removeAll();

      for (let i = 0; i < this.objects.length; i++) {
        const object = this.objects[i];
        const targetObj = targets[i];

        if (!targetObj) continue;

        new TWEEN.Tween(object.position)
          .to(
            {
              x: targetObj.position.x,
              y: targetObj.position.y,
              z: targetObj.position.z,
            },
            Math.random() * duration + duration
          )
          .easing(TWEEN.Easing.Exponential.InOut)
          .start();

        new TWEEN.Tween(object.rotation)
          .to(
            {
              x: targetObj.rotation.x,
              y: targetObj.rotation.y,
              z: targetObj.rotation.z,
            },
            Math.random() * duration + duration
          )
          .easing(TWEEN.Easing.Exponential.InOut)
          .start();
      }

      this.transformTimer = setTimeout(() => {
        target.tween && target.tween(TWEEN);
        resolve();
      }, duration);
    });
  }

  render() {
    this.renderer.render();
  }

  onResize() {
    if (!this.camera || !this.baseRenderer) return;

    this.camera.aspect = window.innerWidth / window.innerHeight;
    this.camera.updateProjectionMatrix();
    this.baseRenderer.setSize(window.innerWidth, window.innerHeight);
    this.render();
  }

  animate = () => {
    this.AnimationFrame = requestAnimationFrame(this.animate);
    TWEEN.update();
    TWEEN2.update();

    this.render();
  };

  destroy() {
    window.removeEventListener("resize", this.onResize.bind(this));
    if (this.AnimationFrame) cancelAnimationFrame(this.AnimationFrame);
    if (this.loopAnimateTimer) clearTimeout(this.loopAnimateTimer);
    if (this.transformTimer) clearTimeout(this.transformTimer);
    this.container.innerHTML = "";
    this.scene = null;
    this.camera = null;
    this.renderer = null;
    this.group = null;
    this.objects = [];
  }
}
